// MIT License (MIT)
// 
// Copyright (c) 2014 K Ercan Turkarslan
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// 

using System;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
using Mios.ExtensionMethods;
using Mios.Generated;
using Mios.MiosConnection;

namespace Mios
{
    public class MiosEngine : BasicMiosEngine
    {
#if USE_SDATA || USE_USERDATA
        public Boolean IsPopulated { get; private set; }

        #region Sections : Array of MiosSection;
        private MiosItemCollection<MiosSection> _mSections;

        public MiosSection[] Sections { get { return _mSections.Items; } }

        public MiosSection GetSectionByName(String name)
        {
            return _mSections.Items.FirstOrDefault(section => section.Name == name);
        }
        #endregion

        #region _mRooms : Array Of MiosRoom;
        private MiosItemCollection<MiosRoom> _mRooms;

        public MiosRoom[] Rooms { get { return _mRooms.Items; } }

        public MiosRoom GetRoomByName(String name)
        {
            return _mRooms.Items.FirstOrDefault(room => room.Name == name);
        }
        #endregion

        #region _mScenes : Array Of MiosScene;
        private MiosItemCollection<MiosScene> _mScenes;

        public MiosScene[] Scenes { get { return _mScenes.Items; } }

        public MiosScene GetSceneByName(String name) 
        {
            return _mScenes.Items.FirstOrDefault(scene => scene.Name == name);
        }
        #endregion

        #region _mDevices : Array Of MiosDevice;
        private MiosItemCollection<MiosDevice> _mDevices;

        public MiosDevice[] Devices { get { return _mDevices.Items; } }

        public MiosDevice GetDeviceByName(String name)
        {
            return _mDevices.Items.FirstOrDefault(device => device.Name == name);
        }
        #endregion

        #region Private method(s)
        private void Initialize()
        {
            _mSections = new MiosItemCollection<MiosSection>();
            _mRooms = new MiosItemCollection<MiosRoom>();
            _mScenes = new MiosItemCollection<MiosScene>();
            _mDevices = new MiosItemCollection<MiosDevice>();
        }
        #endregion

        #region Configuration Parser : Common Code
        private static UInt32 GetId(XNode xNode)
        {
            return UInt32.Parse(xNode.Attributes("id"));
        }

        private MiosRoom GetRoom(XNode xNode)
        {
            UInt32 roomId;

            if (!UInt32.TryParse(xNode.Attributes("room"), out roomId))
                roomId = 0;

            var room = _mRooms.GetById(roomId);
            if (room == null)
                throw new InvalidOperationException("Room not found!");

            return room;
        }

        private void ParseSection(XNode sectionNode)
        {
            _mSections.Add(new MiosSection(this, GetId(sectionNode), sectionNode.Attributes("name")), false);
        }

        private void ParseSections(XDocument xDocument)
        {
            foreach (var childNode in xDocument.Root.GetChildren("sections"))
            {
                ParseSection(childNode);
            }
        }

        private void ParseRoom(XNode roomNode)
        {
            var sectionId = UInt32.Parse(roomNode.Attributes("section"));

            var section = _mSections.GetById(sectionId);
            if (section == null)
                throw new InvalidOperationException("Section not found!");

            _mRooms.Add(new MiosRoom(this, GetId(roomNode), roomNode.Attributes("name"), section), false);
        }

        private void ParseRooms(XDocument xDocument)
        {
            _mRooms.Add(new MiosRoom(this, 0, "No room", _mSections.Items[0]), false);
            foreach (var childNode in xDocument.Root.GetChildren("rooms"))
            {
                ParseRoom(childNode);
            }            
        }

        private void ParseScene(XNode sceneNode)
        {
            _mScenes.Add(new MiosScene(this, GetId(sceneNode), sceneNode.Attributes("name"), GetRoom(sceneNode)), false);            
        }

        private void ParseScenes(XDocument xDocument)
        {
            foreach (var childNode in xDocument.Root.GetChildren("scenes"))
            {
                ParseScene(childNode);
            }            
        }
        #endregion

#if USE_SDATA
        #region Configuration Parser : SData
        const Int32 FindeviceRetrycount = 100;

        private void GetDeviceTypeById(UInt32 id, out String udn, out String deviceType)
        {
            for (var retryCount = FindeviceRetrycount; retryCount > 0; retryCount--)
            {
                try
                {
                    // 20,uuid:4d494342-5342-5645-0014-000001c9d04f,urn:schemas-upnp-org:device:BinaryLight:1
                    var deviceInfo = Luup_FindDevice(id).Split(',');

                    if (deviceInfo[0] == id.ToString(CultureInfo.InvariantCulture))
                    {
                        udn = deviceInfo[1];
                        deviceType = deviceInfo[2];
                        return;
                    }
                }
                catch (MiosConnectionException)
                {
                }
                Thread.Sleep(5000);
            }
            throw new InvalidOperationException("Luup Request [finddevice] did not return expected value!");
        }

        private void ParseDevice_SData(XNode deviceNode)
        {
            var id = GetId(deviceNode);

            String localUdn;
            String deviceType;

            GetDeviceTypeById(id, out localUdn, out deviceType);

            _mDevices.Add(
                MiosDeviceCreator.Create(this, id, deviceNode.Attributes("name"), localUdn, deviceType, GetRoom(deviceNode)),
                false);
        }

        public void PopulateWithSdata(XDocument sData)
        {
            lock (this)
            {
                if (sData == null)
                    throw new ArgumentNullException("sData");

                if (sData.Root == null)
                    throw new ArgumentException("sData.Root");

                Initialize();

                ParseSections(sData);
                ParseRooms(sData);
                ParseScenes(sData);

                foreach (var childNode in sData.Root.GetChildren("devices"))
                {
                    ParseDevice_SData(childNode);
                }

                IsPopulated = true;
            }
        }

        #endregion

        #region Value Parser : SData

        private void ParseDeviceValues_DeviceStatus(MiosDevice miosDevice)
        {
            var deviceStatusXml = Luup_Status(miosDevice.Id, "xml");

            var deviceStatus = XDocument.Parse(deviceStatusXml);

            foreach (var childNode in deviceStatus.Root.GetChildren("states"))
            {
                var serviceId = childNode.Attributes("service");
                var variable = childNode.Attributes("variable");
                var value = childNode.Attributes("value");

                miosDevice.SetVariable(serviceId, variable, value);
            }
        }


        private void ParseDeviceValues_SData(XNode deviceNode)
        {
            var id = GetId(deviceNode);
            var miosDevice = _mDevices.GetById(id);

            foreach (var xAttr in ((XElement) deviceNode).Attributes())
            {
                var attrName = xAttr.Name.LocalName;
                switch (attrName)
                {
                    case "id":
                    case "altid":
                    case "name":
                    case "category":
                    case "subcategory":
                    case "room":
                    case "parent":
                        continue;
                }

                if (miosDevice.SetShortCode(attrName, xAttr.Value))
                {
                    // We have successfully updated the short code.
                    // We can iterate to next shortcode
                    continue;
                }

                ParseDeviceValues_DeviceStatus(miosDevice);

                // If we update the device variables via luup request 'status' for that device
                // we don't need to iterate through rest of the shortcodes.
                break;
            }
        }

        public void UpdateValuesFromSData(XDocument sData)
        {
            lock (this)
            {
                if (!IsPopulated)
                    throw new InvalidOperationException("Configuration is not retrieved from Vera!");

                if (sData == null)
                    throw new ArgumentNullException("sData");

                if (sData.Root == null)
                    throw new ArgumentException("sData.Root");

                foreach (var childNode in sData.Root.GetChildren("devices"))
                {
                    ParseDeviceValues_SData(childNode);
                }
            }
        }

        #endregion
#endif

#if USE_USERDATA
        #region Configuration Parser : User_Data
        private void ParseDevice_UserData(XNode deviceNode)
        {
            _mDevices.Add(
                MiosDeviceCreator.Create(
                                    this,
                                    GetId(deviceNode), 
                                    deviceNode.Attributes("name"), 
                                    deviceNode.Attributes("local_udn"), 
                                    deviceNode.Attributes("device_type"), 
                                    GetRoom(deviceNode)), false);
        }

        public void PopulateWithUserData(XDocument userData)
        {
            lock (this)
            {
                if (userData == null)
                    throw new ArgumentNullException("userData");

                if (userData.Root == null)
                    throw new ArgumentException("userData.Root");

                Initialize();

                ParseSections(userData);
                ParseRooms(userData);
                ParseScenes(userData);

                foreach (var childNode in userData.Root.GetChildren("devices"))
                {
                    ParseDevice_UserData(childNode);
                }

                IsPopulated = true;
            }
        }
        #endregion

        #region Value Parser : Status

        private void ParseDeviceValuesStatus(XNode deviceNode)
        {
            var id = GetId(deviceNode);
            var miosDevice = _mDevices.GetById(id);

            if (miosDevice == null)
                return;
                //throw new InvalidOperationException("miosDevice");

            foreach (var childNode in ((XElement) deviceNode).GetChildren("states"))
            {
                var serviceId = childNode.Attributes("service");
                var variable = childNode.Attributes("variable");
                var value = childNode.Attributes("value");

                miosDevice.SetVariable(serviceId, variable, value);
            }
        }

        public void UpdateValuesFromStatus(XDocument status)
        {
            lock (this)
            {
                if (!IsPopulated)
                    throw new InvalidOperationException("Configuration is not retrieved from Vera!");

                if (status == null)
                    throw new ArgumentNullException("status");

                if (status.Root == null)
                    throw new ArgumentException("status.Root");

                foreach (var childNode in status.Root.GetChildren("devices"))
                {
                    ParseDeviceValuesStatus(childNode);
                }
            }
        }

        #endregion

#endif
#endif
        #region Constructor(s)
        public MiosEngine(MiosController controller)
            : base(controller)
        {
#if USE_SDATA || USE_USERDATA
            IsPopulated = false;

            _mSections = null;
            _mRooms = null;
            _mScenes = null;
            _mDevices = null;
#endif
        }
        #endregion
    }
}
